﻿//------------------------------------------------------------------------------
// <copyright file="GoToILSpyCommand.cs" company="WIndows User">
//     Copyright (c) WIndows User.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace GoToILSpy15
{
	/// <summary>
	/// Command handler
	/// </summary>
	internal sealed class GoToILSpyCommand
	{
		/// <summary>
		/// Command ID.
		/// </summary>
		public const int CommandId = 0x0100;

		/// <summary>
		/// Command menu group (command set GUID).
		/// </summary>
		public static readonly Guid CommandSet = new Guid("aeb8c44e-525e-45e1-829b-8b5b4f2d8b06");

		/// <summary>
		/// VS Package that provides this command, not null.
		/// </summary>
		private readonly Package package;

		/// <summary>
		/// Initializes a new instance of the <see cref="GoToILSpyCommand"/> class.
		/// Adds our command handlers for menu (commands must exist in the command table file)
		/// </summary>
		/// <param name="package">Owner package, not null.</param>
		private GoToILSpyCommand(Package package)
		{
			if (package == null)
			{
				throw new ArgumentNullException("package");
			}

			this.package = package;

			//EnvDTE80.DTE2 _applicationObject = this.ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE80.DTE2;

			OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
			if (commandService != null)
			{
				var menuCommandID = new CommandID(CommandSet, CommandId);
				var menuItem = new MenuCommand(this.MenuItemCallback, menuCommandID);
				commandService.AddCommand(menuItem);
			}
		}

		/// <summary>
		/// Gets the instance of the command.
		/// </summary>
		public static GoToILSpyCommand Instance
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the service provider from the owner package.
		/// </summary>
		private IServiceProvider ServiceProvider
		{
			get
			{
				return this.package;
			}
		}

		/// <summary>
		/// Initializes the singleton instance of the command.
		/// </summary>
		/// <param name="package">Owner package, not null.</param>
		public static void Initialize(Package package)
		{
			Instance = new GoToILSpyCommand(package);
		}

		/// <summary>
		/// This function is the callback used to execute the command when the menu item is clicked.
		/// See the constructor to see how the menu item is associated with this function using
		/// OleMenuCommandService service and MenuCommand class.
		/// </summary>
		/// <param name="sender">Event sender.</param>
		/// <param name="e">Event args.</param>
		private void MenuItemCallback(object sender, EventArgs e)
		{
			//https://vlasovstudio.com/visual-commander/commands.html#CreateTypedVariable
			//https://www.mztools.com/articles/2014/MZ2014020.aspx
			//http://stackoverflow.com/questions/24163986/how-to-get-current-activedocument-in-visual-studio-extension-using-mef
			//http://stackoverflow.com/questions/2868127/get-the-selected-text-of-the-editor-window-visual-studio-extension
			//http://www.fmtconsultants.com/creating-a-visual-studio-extension-part-2-of-2/

			try
			{
				string ilSpyPath = ((OptionPageGrid)package.GetDialogPage(typeof(OptionPageGrid)))?.IlSpyPath;
				if (string.IsNullOrWhiteSpace(ilSpyPath))
				{
					ShowInfo("Tool haven't been configured. Please navigate to Tools / Options / Go to ILSpy and set ILSpy path.");
					return;
				}

				Microsoft.VisualStudio.Text.Editor.IWpfTextView textView = GetTextView();
				Microsoft.VisualStudio.Text.SnapshotPoint caretPosition = textView.Caret.Position.BufferPosition;
				Document document = caretPosition.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
				if (document == null)
					ShowInfo("Execute the function while a document window is active.");
				SyntaxNode rootSyntaxNode = document.GetSyntaxRootAsync().Result;
				SyntaxToken st = rootSyntaxNode.FindToken(caretPosition);
				SemanticModel semanticModel = document.GetSemanticModelAsync().Result;
				SymbolInfo si = semanticModel.GetSymbolInfo(st.Parent);
				ISymbol symbol = si.Symbol ?? (si.GetType().GetProperty("CandidateSymbols").GetValue(si) as IEnumerable<ISymbol>)?.FirstOrDefault(); /*?? si.CandidateSymbols.FirstOrDefault()*/

				PreprocessSymbol(ref symbol);

				INamedTypeSymbol typeSymbol = null;
				string memberName = null;
				MemberType memberType = (MemberType)0;
				if ((symbol == null) || ((!TryHandleAsType(symbol, out typeSymbol)) && (!TryHandleAsMember(symbol, out typeSymbol, out memberName, out memberType))))
				{
					ShowInfo($"{st.Text} is not a valid identifier.");
					return;
				}

				string typeName = GetTypeName(typeSymbol);
				string asmDef = GetAssemblyDefinition(typeSymbol.ContainingAssembly);
				string asmPath = GetAssemblyPath(semanticModel, asmDef);
				if (asmPath == null)
				{
					ShowInfo($"Assembly '{asmDef}' not found; {typeName} is not referenced assembly type.");
					return;
				}
				asmPath = GetRealAssemblyPath(asmPath);

				try
				{
					new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { Arguments = BuildIlSpyArguments(asmPath, typeName, memberName, memberType), FileName = ilSpyPath } }.Start();
					//ShowInfo("Opening ILSpy: " + BuildIlSpyAruments(asmPath, typeName, memberName, memberType));
				}
				catch (Exception ex)
				{
					ShowInfo("Error opening ILSpy: " + ex.Message);
				}
			}
			catch (Exception ex)
			{
				ShowInfo("GoToILSpy error: " + ex.Message);
			}
		}

		#region source code handle helpers
		private Microsoft.VisualStudio.Text.Editor.IWpfTextView GetTextView()
		{
			/*EnvDTE.Document doc=GetRequiredService<EnvDTE80.DTE2, EnvDTE.DTE>().ActiveDocument;
			EnvDTE.TextDocument td=doc.Object() as EnvDTE.TextDocument;
			object o=doc.ActiveWindow.Selection;
			EnvDTE.TextSelection ts=doc.Selection as EnvDTE.TextSelection;
			string s=td.CreateEditPoint(td.StartPoint).GetText(td.EndPoint);
			int pos = ts.ActivePoint.AbsoluteCharOffset;*/

			Microsoft.VisualStudio.TextManager.Interop.IVsTextManager textManager =
				GetRequiredService<Microsoft.VisualStudio.TextManager.Interop.IVsTextManager, Microsoft.VisualStudio.TextManager.Interop.SVsTextManager>();
			Microsoft.VisualStudio.TextManager.Interop.IVsTextView textView;
			textManager.GetActiveView(1, null, out textView);
			return GetEditorAdaptersFactoryService().GetWpfTextView(textView);
		}

		private Microsoft.VisualStudio.Editor.IVsEditorAdaptersFactoryService GetEditorAdaptersFactoryService()
		{
			Microsoft.VisualStudio.ComponentModelHost.IComponentModel componentModel =
				GetRequiredService<Microsoft.VisualStudio.ComponentModelHost.IComponentModel, Microsoft.VisualStudio.ComponentModelHost.SComponentModel>();
			return componentModel.GetService<Microsoft.VisualStudio.Editor.IVsEditorAdaptersFactoryService>();
		}

		T GetRequiredService<T, TServiceType>()
		{
			return (T)ServiceProvider.GetService(typeof(TServiceType));
		}
		#endregion source code handle helpers

		#region PreprocessSymbol
		static void PreprocessSymbol(ref ISymbol symbol)
		{
			if (TryPreprocessLocal(ref symbol))
				return;
		}

		static bool TryPreprocessLocal(ref ISymbol symbol)
		{
			ILocalSymbol loc = symbol as ILocalSymbol;
			bool result = loc != null;
			if (result)
				symbol = loc.Type;
			return result;
		}
		#endregion PreprocessSymbol

		#region process symbol
		static bool TryHandleAsType(ISymbol symbol, out INamedTypeSymbol type)
		{
			type = symbol as INamedTypeSymbol;
			return type != null;
		}

		static bool TryHandleAsMember(ISymbol symbol, out INamedTypeSymbol type, out string memberName, out MemberType memberType)
		{
			type = null;
			memberName = null;
			memberType = 0;

			IFieldSymbol fieldSymbol = symbol as IFieldSymbol;
			if (fieldSymbol != null)
			{
				type = fieldSymbol.ContainingType;
				memberName = fieldSymbol.Name;
				memberType = MemberType.Field;
			}
			else
			{
				IPropertySymbol propSymbol = symbol as IPropertySymbol;
				if (propSymbol != null)
				{
					type = propSymbol.ContainingType;
					memberName = propSymbol.Name;
					memberType = MemberType.Property;
				}
				else
				{
					IMethodSymbol methSymbol = symbol as IMethodSymbol;
					if (methSymbol != null)
					{
						type = methSymbol.ContainingType;
						memberName = methSymbol.Name;
						memberType = MemberType.Method;
					}
					else
					{
						IEventSymbol eventSymbol = symbol as IEventSymbol;
						if (eventSymbol != null)
						{
							type = eventSymbol.ContainingType;
							memberName = eventSymbol.Name;
							memberType = MemberType.Event;
						}
					}
				}
			}
			return type != null;
		}

		enum MemberType { Field, Property, Method, Event }
		#endregion process symbol

		static string GetAssemblyPath(SemanticModel semanticModel, string assemblyDef)
		{
			IEnumerator<AssemblyIdentity> refAsmNames = semanticModel.Compilation.ReferencedAssemblyNames.GetEnumerator();
			IEnumerator<MetadataReference> refs = semanticModel.Compilation.References.GetEnumerator();

			while (refAsmNames.MoveNext())
			{
				refs.MoveNext();
				if (refAsmNames.Current.ToString() == assemblyDef)
					return refs.Current.Display;
			}
			return null;
		}

		static string BuildIlSpyArguments(string asmPath, string typeName, string memberName, MemberType memberType)
		{
			//ILSpy.exe MyAssembly.dll /navigateTo:M:MyClass.MyMethod 1( 0)
			//c:\Program Files\ILSpy > ilspy C:\WINDOWS\Microsoft.Net\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll / navigateTo:P: System.Web.HttpApplication.Events
			//T:, P:, F:, E:, M:, 
			//https://github.com/icsharpcode/ILSpy/blob/master/ILSpy/XmlDoc/XmlDocKeyProvider.cs

			string result = $"\"{asmPath}\" /navigateTo:{(memberName == null ? 'T' : memberType.ToString()[0])}:{typeName}";
			if (memberName != null)
				result = $"{result}.{memberName}";
			return result;
		}

		static string GetRealAssemblyPath(string asmPath)
		{
			try
			{
				return Assembly.LoadFile(asmPath)?.Location ?? asmPath;
			}
			catch
			{ }
			return asmPath;
		}

		static string GetTypeName(INamedTypeSymbol typeSymbol)
		{
			INamespaceSymbol nsSym = typeSymbol.ContainingNamespace;
			StringBuilder sb = new StringBuilder();
			while ((nsSym != null) && (!nsSym.IsGlobalNamespace))
			{
				if (sb.Length == 0)
					sb.Append(nsSym.Name).Append('.');
				else
					sb.Insert(0, '.').Insert(0, nsSym.Name);
				nsSym = nsSym.ContainingNamespace;
			}
			return sb.Append(typeSymbol.MetadataName).ToString();
		}

		static string GetAssemblyDefinition(IAssemblySymbol assemblySymbol)
		{
			return assemblySymbol.Identity.ToString();
		}

		#region status
		IVsStatusbar _statusBar;
		void ShowInfo(string text)
		{
			if (_statusBar == null)
				try
				{
					_statusBar = GetRequiredService<IVsStatusbar, SVsStatusbar>();
				}
				catch
				{
					MessageBox.Show(text);
					return;
				}

			_statusBar.SetText(text);
		}
		#endregion status
	}
}
